﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows;
using System.Windows.Navigation;
using System.Windows.Resources;


namespace IAllen.Silverlight.Controls.Navigation
{
    public class ModuleContentLoader : INavigationContentLoader
    {
        #region Const

        private const string External = ";external/";
        private const string Component = ";component/";

        private const string UriPattern = "/([A-Za-z0-9._]+)" 
            + External + "([A-Za-z0-9._]+)" + Component + "([A-Za-z0-9._/]+)";
        private const string XClassPattern = ".*x:Class=\"(.*?)\"";
        private const string KeyFormat = "{0}&{1}";

        #endregion

        #region Fields

        private readonly PageResourceContentLoader _traditionalLoader;

        private static readonly object SyncLocker;
        private static readonly Dictionary<string, Assembly> InnerTable;

        #endregion

        #region Constructors
        /// <summary>
        /// Format is as follows：
        /// /{xap/dll file name};external/{assembly name};component/{xaml page's path}.xaml
        /// </summary>
        public ModuleContentLoader()
        {
            _traditionalLoader = new PageResourceContentLoader();
        }

        static ModuleContentLoader()
        {
            SyncLocker = new object();
            InnerTable = new Dictionary<string, Assembly>();
        }

        #endregion

        #region Implements

        public IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState)
        {
            if (targetUri == null) throw new ArgumentNullException("targetUri");
            var uri = targetUri.OriginalString;

            //If it's a traditional uri, we'll use PageResourceContentLoader,
            //otherwise, we'll use ModuleContentLoader.
            if (!uri.Contains(External))
                return _traditionalLoader.BeginLoad(targetUri, currentUri, userCallback, asyncState);

            var matchGroup = new Regex(UriPattern).Match(uri).Groups;
            
            if (4 != matchGroup.Count)
                throw new UriFormatException("The format of targetUri is incorrect.");

            var asyncResult = new NavigationAsyncResult(userCallback, asyncState);

            var module = new ModuleInfo(
                matchGroup[1].Value,
                matchGroup[2].Value,
                matchGroup[3].Value) { AsyncResult = asyncResult };

            LoadXapOrDll(module);
            return asyncResult;
        }

        public bool CanLoad(Uri targetUri, Uri currentUri)
        {
            if (targetUri == null) throw new ArgumentNullException("targetUri");
            var uri = targetUri.OriginalString;

            if (uri.Contains(External))
                return true;
            return _traditionalLoader.CanLoad(targetUri, currentUri);
        }

        public void CancelLoad(IAsyncResult asyncResult) { /*Do Nothing*/ }

        public LoadResult EndLoad(IAsyncResult asyncResult)
        {
            if (asyncResult is NavigationAsyncResult)
                return new LoadResult(((NavigationAsyncResult)asyncResult).Result);
            return _traditionalLoader.EndLoad(asyncResult);
        }

        #endregion

        #region Private Methods

        private static void LoadXapOrDll(ModuleInfo module)
        {
            var key = String.Format(KeyFormat, module.ExternalModuleName, module.ComponentName);
            if (InnerTable.ContainsKey(key))
            {
                var assembly = InnerTable[key];

                module.AsyncResult.Result = assembly.CreateInstance(GetXamlType(module));
                module.AsyncResult.Completed();
                return;
            }

            //Download a xap/dll file from server.
            var proxy = new WebClient();
            proxy.OpenReadCompleted += LoadXapOrDllCompleted;
            proxy.OpenReadAsync(new Uri(module.ExternalModuleName, UriKind.Relative), module);
        }

        /// <summary>
        /// OpenReadCompleted Event Handler.
        /// </summary>
        /// <param name="sender">WebClient instance</param>
        /// <param name="e">OpenReadCompletedEventArgs instance</param>
        private static void LoadXapOrDllCompleted(object sender, OpenReadCompletedEventArgs e)
        {
            var module = e.UserState as ModuleInfo;

            if (e.Error != null || e.Cancelled || module == null) return;

            StreamResourceInfo dllResourceInfo;

            if (module.IsDllFile)
                dllResourceInfo = new StreamResourceInfo(e.Result, "application/octet-stream");
            else
                dllResourceInfo = Application.GetResourceStream(
                    new StreamResourceInfo(e.Result, "application/x-silverlight-app"),
                    new Uri(module.ComponentName + ".dll", UriKind.Relative));

            if (dllResourceInfo == null)
                throw new NullReferenceException(
                    "Can't find the resource, or Uri you typed could be incorrect.");

            var part = new AssemblyPart();
            var assembly = part.Load(dllResourceInfo.Stream);

            module.AsyncResult.Result = assembly.CreateInstance(GetXamlType(module));
            module.AsyncResult.Completed();

            lock (SyncLocker)
            {
                InnerTable.Add(
                    String.Format(KeyFormat, module.ExternalModuleName, module.ComponentName),
                    assembly);
            }
        }

        /// <summary>
        /// Get page's type from xaml.
        /// </summary>
        /// <param name="module">ModuleInfo</param>
        /// <returns>Type name of a page we'll navigate to</returns>
        private static string GetXamlType(ModuleInfo module)
        {
            var relativeUri = new Uri(Uri.EscapeUriString(module.ComponentName)
                                      + Component
                                      + Uri.EscapeUriString(module.RelativeUri), UriKind.Relative);
            var xamlInfo = Application.GetResourceStream(relativeUri);

            if (xamlInfo == null) throw new NullReferenceException(
                "Can't find the resource, or Uri you typed could be incorrect.");

            string xaml;
            using (var reader = new StreamReader(xamlInfo.Stream))
            {
                var regexClass = new Regex(XClassPattern, RegexOptions.CultureInvariant);
                while (null != (xaml = reader.ReadLine()))
                {
                    var match = regexClass.Match(xaml);
                    if (match != Match.Empty)
                    {
                        xaml = match.Groups[1].Value;
                        break;
                    }
                }
                reader.Close();
            }

            return xaml;
        }

        #endregion

        #region Inner Class: NavigationAsyncResult

        internal class NavigationAsyncResult : IAsyncResult
        {
            #region Fields

            private readonly AsyncCallback _callback;
            private readonly object _state;
            private bool _isCompleted;
            private readonly object _syncLocker = new object();

            #endregion

            #region Constructor

            internal NavigationAsyncResult(AsyncCallback callback, object asyncState)
            {
                _callback = callback;
                _state = asyncState;
                _isCompleted = false;
            }

            #endregion

            #region Implements

            public bool IsCompleted
            {
                get { return _isCompleted; }
            }

            public WaitHandle AsyncWaitHandle
            {
                get { throw new NotSupportedException(); }
            }

            public object AsyncState
            {
                get { return _state; }
            }

            public bool CompletedSynchronously
            {
                get { return false; }
            }

            #endregion

            /// <summary>
            /// Reference a page instance.
            /// </summary>
            internal object Result { get; set; }

            /// <summary>
            /// After finishing downloading the Xap/Dll file, it'll be invoked.
            /// </summary>
            internal void Completed()
            {
                lock (_syncLocker)
                {
                    _isCompleted = true;
                }
                if (_callback != null)
                {
                    _callback(this);
                }
            }
        }

        #endregion

        #region Inner Class: ModuleInfo

        internal class ModuleInfo
        {
            #region Constructor

            internal ModuleInfo(string moduleName, string componentName, string relativeUri)
            {
                if (String.IsNullOrEmpty(moduleName))
                    throw new ArgumentNullException("moduleName");

                if (String.IsNullOrEmpty(componentName))
                    throw new ArgumentNullException("componentName");

                if (String.IsNullOrEmpty(relativeUri))
                    throw new ArgumentNullException("relativeUri");

                ExternalModuleName = moduleName.ToLower();
                ComponentName = componentName.ToLower();
                RelativeUri = relativeUri.ToLower();

                if (ExternalModuleName.IndexOf(".xap") > 0)
                {
                    IsDllFile = false;
                }
                else if (ExternalModuleName.IndexOf(".dll") > 0)
                {
                    IsDllFile = true;
                }
                else
                    throw new ArgumentException("Module's name may be wrong. Please check the typed file name.");
            }

            #endregion

            #region Properties
            
            /// <summary>
            /// Xap or Dll file name
            /// </summary>
            internal string ExternalModuleName { get; private set; }

            /// <summary>
            /// Assembly name
            /// </summary>
            internal string ComponentName { get; private set; }

            /// <summary>
            /// Xaml page's path
            /// </summary>
            internal string RelativeUri { get; private set; }

            /// <summary>
            /// A NavigationAsyncResult instance
            /// </summary>
            internal NavigationAsyncResult AsyncResult { get; set; }

            /// <summary>
            /// Whether it's a dll file.
            /// </summary>
            internal bool IsDllFile { get; private set; }

            #endregion
        }

        #endregion
    }
}
